﻿using System;
using System.Collections.Generic;
using System.Linq;
using BigData.DataContracts.Objects.BigData;
using BigDataFramework.Repositories;
using MongoDB.Bson;
using BigDataFramework.Library.Helpers;
using BigDataFramework.Repositories.Interfaces;

namespace BigDataFramework.Library.Objects.DataAdapters
{
    /// <summary>
    /// The base class which all data adapters should extend from
    /// </summary>
    public abstract class DataAdapterBase<T>
        where T : BigData.DataContracts.Objects.BigData.BaseTypes.BigDataDBObject_FiniteLife
    {
        #region Members
        private ObjectId _registeredCollectionId;
        protected IRepository _dbContext { get; set; }
        #endregion Members
        #region Properties
        /// <summary>
        /// Represents the RegisteredCollection object that this DataAdapter is associated with
        /// </summary>
        public ObjectId RegisteredCollectionId
        {
            get
            {
                return _registeredCollectionId;
            }
        }
        #endregion Properties
        #region Constructor
        protected DataAdapterBase()
        {
            this._dbContext = new MongoDBRepository();
            // Rerieve the registered collection id
            this._registeredCollectionId = ObjectId.Parse(this._dbContext.ReadOne<RegisteredCollection>(rc => rc.ObjectTypeName == typeof(T).FullName)._id);
        }
        #endregion Constructor
        #region Protected Methods
        /// <summary>
        /// Retrieves data from the data source
        /// </summary>
        /// <param name="userIdentifier">An identifier which represents the user to run the report for</param>
        /// <param name="startDate">The date for which the report should start</param>
        /// <param name="endDate">The date for which the report should end</param>
        /// <returns>A JSON representation of the data which is returned</returns>
        protected abstract List<T> RetrieveDataFromAPIForUser(BigDataUser user, DateTime startDate, DateTime endDate);
        #endregion Protected Methods
        #region Public Methods
        /// <summary>
        /// Converts an object of type T to a BigData.DataContracts.Objects.BigData.Event
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public abstract Event ConvertObjToEvent(T obj);

        /// <summary>
        /// Converts a list of objects of type T to a BigData.DataContracts.Objects.BigData.Event
        /// </summary>
        /// <param name="objs"></param>
        /// <returns></returns>
        public List<Event> ConvertObjToEvent(List<T> objs)
        {
            return objs.Select<T, Event>(o => this.ConvertObjToEvent(o)).ToList();
        }

        /// <summary>
        /// Returns a list of all the objects which were updated since the last sync time for the specified user
        /// </summary>
        /// <param name="user">The user to pull data from</param>
        /// <param name="shouldUpdateLastSyncDate">should last sync date be updated?</param>
        /// <returns>the list of objects created since the last sync date</returns>
        public List<T> GetDataUpdatesSinceLastSync(BigDataUser user, bool shouldUpdateLastSyncDate = false)
        {
            List<BigDataUserToRegisteredCollection> userToCollections;
            BigDataUserToRegisteredCollection userToCollection;
            DateTime startDate;
            DateTime endDate;
            List<T> output;

            // Retrieve the BigDataUserToRegisteredCollection object from the database
            userToCollections = this._dbContext.Read<BigDataUserToRegisteredCollection>(bdutrc =>
                        bdutrc.BigDataUserId == ObjectId.Parse(user._id)
                    &&  bdutrc.RegisteredCollectionId == this.RegisteredCollectionId
                ).ToList();

            // If none were retrieved, then create one
            if (userToCollections.Count == 0)
            {
                userToCollection = new BigDataUserToRegisteredCollection()
                {
                    BigDataUserId = ObjectId.Parse(user._id),
                    LastDataSync = DateTime.MinValue,
                    RecordEndDate = DateTime.MaxValue,
                    RecordStartDate = DateTime.Today,
                    RegisteredCollectionId = this.RegisteredCollectionId
                };

                this._dbContext.Create(userToCollection);
                userToCollections.Add(userToCollection);
            }

            // If only one was pulled, then set it
            else if (userToCollections.Count == 1)
                userToCollection = userToCollections.Single();
            else // If more than one were pulled, then throw and error
                throw new Exception(string.Format("Only one BigDataUserToRegisteredCollection is allowed for the combination of BigDataUser {0} and BigDataRegisteredCollection {1}", ObjectId.Parse(user._id), this.RegisteredCollectionId));

            startDate = userToCollection.LastDataSync;
            endDate = DateTime.Now.ToUniversalTime();

            output = this.RetrieveDataFromAPIForUser(user, startDate, endDate);

            // Update the userToCollection object with the newest LastDataSync
            if (shouldUpdateLastSyncDate)
            {
                userToCollection.LastDataSync = endDate;
                this._dbContext.Update(userToCollection);
            }

            return output;
        }
        /// <summary>
        /// Gets the events created between the passed in start date and end date
        /// </summary>
        /// <param name="reportStartDate"></param>
        /// <param name="reportEndDate"></param>
        /// <returns></returns>
        public List<Event> GetCreatedEvents(DateTime reportStartDate, DateTime reportEndDate)
        {
            // Pull all the objects which have been startDated between the reportStartDate and the reportEndDate.
            return this.ConvertObjToEvent(this._dbContext.Read<T>(t =>
                            t.RecordStartDate >= reportStartDate
                        &&  t.RecordStartDate <= reportEndDate
                    ).ToList());
        }
        /// <summary>
        /// Gets the events enddated between the passed in start date and end date
        /// </summary>
        /// <param name="reportStartDate"></param>
        /// <param name="reportEndDate"></param>
        /// <returns></returns>
        public List<string> GetDeletedEvents(DateTime reportStartDate, DateTime reportEndDate)
        {
            // Pull all the objects which have been endDated between the reportStartDate and the reportEndDate.
            return this._dbContext.Read<T>(t =>
                            t.RecordEndDate >= reportStartDate
                        &&  t.RecordEndDate <= reportEndDate
                    ).Select(t => t._id).ToList();
        }
        /// <summary>
        /// Processes all of the registered collections for Event objects which it then stores in the database
        /// </summary>
        public void SyncDataToEvents()
        {
            RegisteredCollection registeredCollection;
            DateTime startDate;
            DateTime endDate;
            List<Event> createdEvents;
            Event e;

            registeredCollection = this._dbContext.ReadOne<RegisteredCollection>(rc => rc.ObjectTypeName == typeof(T).FullName);

            startDate = registeredCollection.LastEventSync;
            endDate = DateTime.Now;

            createdEvents = GetCreatedEvents(startDate, endDate);
            // Create any new events
            this._dbContext.Create<Event>(createdEvents);

            // EndDate any "to be enddated events"
            foreach (string oid in GetDeletedEvents(startDate, endDate))
            {
                // Pull the object
                e = this._dbContext.ReadOne<Event>(oid);
                // Enddate the object
                e.RecordEndDate = endDate;
                // Commit the changes
                this._dbContext.Update<Event>(e);
            }

            // Update the last sync date
            registeredCollection.LastEventSync = endDate;

            this._dbContext.Update<RegisteredCollection>(registeredCollection);
        }
        #endregion Public Methods
    }
}